﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using Xamarin.Forms;
using Xamarin.Forms.Internals;

namespace SynodeTechnologies.SkiaSharp.DiagramEngine.Internals
{
    internal sealed class ListProxy : IList,IReadOnlyList<object>, INotifyCollectionChanged
    {
        readonly ICollection _collection;
        readonly IList _list;
        readonly ConditionalWeakTable<ListProxy, WeakNotifyProxy> _sourceToWeakHandlers;

        IEnumerator _enumerator;
        int _enumeratorIndex;

        bool _finished;
        HashSet<int> _indexesCounted;

        Dictionary<int, object> _items;
        int _version;

        internal ListProxy(IEnumerable enumerable)
        {
            ProxiedEnumerable = enumerable;
            _collection = enumerable as ICollection;
            _sourceToWeakHandlers = new ConditionalWeakTable<ListProxy, WeakNotifyProxy>();

            if (_collection == null && enumerable is IReadOnlyCollection<object>)
                _collection = new ReadOnlyListAdapter((IReadOnlyCollection<object>)enumerable);

            _list = enumerable as IList;
            if (_list == null && enumerable is IReadOnlyList<object>)
                _list = new ReadOnlyListAdapter((IReadOnlyList<object>)enumerable);

            var changed = enumerable as INotifyCollectionChanged;
            if (changed != null)
                _sourceToWeakHandlers.Add(this, new WeakNotifyProxy(this, changed));
        }

        public IEnumerable ProxiedEnumerable { get; }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public IEnumerator<object> GetEnumerator()
        {
            return new ProxyEnumerator(this);
        }

        /// <summary>
        ///     Gets whether or not the current window contains the <paramref name="item" />.
        /// </summary>
        /// <param name="item">The item to search for.</param>
        /// <returns><c>true</c> if the item was found in a list or the current window, <c>false</c> otherwise.</returns>
        public bool Contains(object item)
        {
            if (_list != null)
                return _list.Contains(item);

            EnsureWindowCreated();

            if (_items != null)
                return _items.Values.Contains(item);

            return false;
        }

        /// <summary>
        ///     Gets the index for the <paramref name="item" /> if in a list or the current window.
        /// </summary>
        /// <param name="item">The item to search for.</param>
        /// <returns>The index of the item if in a list or the current window, -1 otherwise.</returns>
        public int IndexOf(object item)
        {
            if (_list != null)
                return _list.IndexOf(item);

            EnsureWindowCreated();

            if (_items != null)
            {
                foreach (KeyValuePair<int, object> kvp in _items)
                {
                    if (Equals(kvp.Value, item))
                        return kvp.Key;
                }
            }

            return -1;
        }

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        public int Count
        {
            get
            {
                if (_collection != null)
                    return _collection.Count;

                EnsureWindowCreated();

                if (_indexesCounted != null)
                    return _indexesCounted.Count;

                return 0;
            }
        }

        public object this[int index]
        {
            get
            {
                object value;
                if (!TryGetValue(index, out value))
                    throw new ArgumentOutOfRangeException("index");

                return value;
            }
        }

        public void Clear()
        {
            _version++;
            _finished = false;
            _enumeratorIndex = 0;

            if (_enumerator != null)
            {
                var dispose = _enumerator as IDisposable;
                if (dispose != null)
                    dispose.Dispose();

                _enumerator = null;
            }

            if (_items != null)
                _items.Clear();
            if (_indexesCounted != null)
                _indexesCounted.Clear();

            OnCountChanged();
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        public event EventHandler CountChanged;

        
        bool CountIndex(int index)
        {
            if (_collection != null)
                return false;

            // A collection is used in case TryGetValue is called out of order.
            if (_indexesCounted == null)
                _indexesCounted = new HashSet<int>();

            if (_indexesCounted.Contains(index))
                return false;

            _indexesCounted.Add(index);
            return true;
        }

        void EnsureWindowCreated()
        {
            if (_items != null && _items.Count > 0)
                return;

            object value;
            TryGetValue(0, out value);
        }

        void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            Action action;
            if (_list == null)
            {
                action = Clear;
            }
            else
            {
                action = () =>
                {
                    _version++;
                    OnCollectionChanged(e);
                };
            }

            e = e.WithCount(Count);
            if (Device.IsInvokeRequired)
                Device.BeginInvokeOnMainThread(action);
            else
                action();
        }

        void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            CollectionChanged?.Invoke(this, e);
        }

        void OnCountChanged()
        {
            CountChanged?.Invoke(this, EventArgs.Empty);
        }

        bool TryGetValue(int index, out object value)
        {
            value = null;

            if (_list != null)
            {
                object indexedValue = null;
                var inRange = false;
                Action getFromList = () =>
                {
                    if (index >= _list.Count)
                        return;

                    indexedValue = _list[index];
                    inRange = true;
                };

                getFromList();

                value = indexedValue;
                return inRange;
            }

            if (_collection != null && index >= _collection.Count)
                return false;
            if (_items != null)
            {
                bool found = _items.TryGetValue(index, out value);
                if (found || _finished)
                    return found;
            }

            if (_enumerator == null)
                _enumerator = ProxiedEnumerable.GetEnumerator();
            if (_items == null)
                _items = new Dictionary<int, object>();

            var countChanged = false;

            for (; _enumeratorIndex < Count; _enumeratorIndex++)
            {
                var moved = false;
                Action move = () =>
                {
                    try
                    {
                        moved = _enumerator.MoveNext();
                    }
                    catch (InvalidOperationException ioex)
                    {
                        throw new InvalidOperationException("You must call UpdateNonNotifyingList() after updating a list that does not implement INotifyCollectionChanged", ioex);
                    }

                    if (!moved)
                    {
                        var dispose = _enumerator as IDisposable;
                        if (dispose != null)
                            dispose.Dispose();

                        _enumerator = null;
                        _enumeratorIndex = 0;
                        _finished = true;
                    }
                };

                
                move();

                if (!moved)
                    break;

                if (CountIndex(_enumeratorIndex))
                    countChanged = true;

                 _items.Add(_enumeratorIndex, _enumerator.Current);
            }

            if (countChanged)
                OnCountChanged();

            return _items.TryGetValue(index, out value);
        }

        class WeakNotifyProxy
        {
            readonly WeakReference<INotifyCollectionChanged> _weakCollection;
            readonly WeakReference<ListProxy> _weakProxy;
            readonly ConditionalWeakTable<ListProxy, NotifyCollectionChangedEventHandler> _sourceToWeakHandlers;

            public WeakNotifyProxy(ListProxy proxy, INotifyCollectionChanged incc)
            {
                _sourceToWeakHandlers = new ConditionalWeakTable<ListProxy, NotifyCollectionChangedEventHandler>();
                NotifyCollectionChangedEventHandler handler = new NotifyCollectionChangedEventHandler(OnCollectionChanged);

                _sourceToWeakHandlers.Add(proxy, handler);
                incc.CollectionChanged += handler;

                _weakProxy = new WeakReference<ListProxy>(proxy);
                _weakCollection = new WeakReference<INotifyCollectionChanged>(incc);
            }

            void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                ListProxy proxy;
                if (!_weakProxy.TryGetTarget(out proxy))
                {
                    INotifyCollectionChanged collection;
                    if (_weakCollection.TryGetTarget(out collection))
                        collection.CollectionChanged -= OnCollectionChanged;

                    return;
                }

                proxy.OnCollectionChanged(sender, e);
            }
        }

        class ProxyEnumerator : IEnumerator<object>
        {
            readonly ListProxy _proxy;
            readonly int _version;

            int _index;

            public ProxyEnumerator(ListProxy proxy)
            {
                _proxy = proxy;
                _version = proxy._version;
            }

            public void Dispose()
            {
            }

            public bool MoveNext()
            {
                if (_proxy._version != _version)
                    throw new InvalidOperationException();

                object value;
                bool next = _proxy.TryGetValue(_index++, out value);
                if (next)
                    Current = value;

                return next;
            }

            public void Reset()
            {
                _index = 0;
                Current = null;
            }

            public object Current { get; private set; }
        }

        #region IList

        object IList.this[int index]
        {
            get { return this[index]; }
            set { throw new NotSupportedException(); }
        }

        bool IList.IsReadOnly
        {
            get { return true; }
        }

        bool IList.IsFixedSize
        {
            get { return false; }
        }

        bool ICollection.IsSynchronized
        {
            get { return false; }
        }

        object ICollection.SyncRoot
        {
            get { throw new NotSupportedException(); }
        }

        void ICollection.CopyTo(Array array, int index)
        {
            throw new NotSupportedException();
        }

        int IList.Add(object item)
        {
            throw new NotSupportedException();
        }

        void IList.Remove(object item)
        {
            throw new NotSupportedException();
        }

        void IList.Insert(int index, object item)
        {
            throw new NotSupportedException();
        }

        void IList.RemoveAt(int index)
        {
            throw new NotSupportedException();
        }

        void IList.Clear()
        {
            throw new NotSupportedException();
        }
        #endregion
    }
}
